package org.luckyjourney.schedul;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.luckyjourney.constant.AuditStatus;
import org.luckyjourney.constant.RedisConstant;
import org.luckyjourney.entity.Setting;
import org.luckyjourney.entity.video.Video;
import org.luckyjourney.entity.vo.HotVideo;
import org.luckyjourney.service.SettingService;
import org.luckyjourney.service.video.VideoService;
import org.luckyjourney.util.RedisCacheUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @description: 热度排行榜
 * @Author: Fxy
 * @CreateTime: 2023-10-31 00:50
 */
@Component
@Slf4j
public class HotRank {

    @Autowired
    private VideoService videoService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SettingService settingService;

    @Autowired
    private RedisCacheUtil redisCacheUtil;

    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();

    {
        jackson2JsonRedisSerializer.setObjectMapper(om);
    }

    /**
     * 热度排行榜 亮点：topk,快速切片扫描全表
     */
    @Scheduled(cron = "0 0 */1 * * ?")
    public void hotRank() {
        // 控制数量
        /**
         new PriorityQueue<HotVideo>(10, Comparator.comparing(HotVideo::getHot))是创建一个优先队列，用于存储HotVideo对象。队列的容量是10，表示它最多可以存储10个元素。
         Comparator.comparing(HotVideo::getHot)是一个比较器，用于比较两个HotVideo对象的热度值(getHot)。优先队列会根据这个比较器来决定元素的排序。 默认是小顶堆，即队头元素（堆顶元素）是队列中最小的
         */
        final TopK topK = new TopK(10, new PriorityQueue<HotVideo>(10, Comparator.comparing(HotVideo::getHot)));
        long limit = 1000;
        // 每次拿1000个
        long id = 0;
        List<Video> videos = videoService.list(new LambdaQueryWrapper<Video>()
                .select(Video::getId, Video::getShareCount, Video::getHistoryCount, Video::getStartCount, Video::getFavoritesCount,
                        Video::getGmtCreated, Video::getTitle).gt(Video::getId, id)
                .eq(Video::getAuditStatus, AuditStatus.SUCCESS).eq(Video::getOpen, 0).last("limit " + limit));

        while (!ObjectUtils.isEmpty(videos)) {
            for (Video video : videos) {
                Long shareCount = video.getShareCount();
                Double historyCount = video.getHistoryCount() * 0.8;
                Long startCount = video.getStartCount();
                Double favoritesCount = video.getFavoritesCount() * 1.5;
                final Date date = new Date();
                long t = date.getTime() - video.getGmtCreated().getTime();
                // 随机获取6位数,用于去重
                final double v = weightRandom();
                final double hot = hot(shareCount + historyCount + startCount + favoritesCount + v, TimeUnit.MILLISECONDS.toDays(t));
                //将毫秒数转换为天数的
                final HotVideo hotVideo = new HotVideo(hot, video.getId(), video.getTitle());
                topK.add(hotVideo);
            }
            id = videos.get(videos.size() - 1).getId();
            videos = videoService.list(new LambdaQueryWrapper<Video>().gt(Video::getId, id).last("limit " + limit));
            ;
        }
        final byte[] key = RedisConstant.HOT_RANK.getBytes();
        final List<HotVideo> hotVideos = topK.get();
        final Double minHot = hotVideos.get(0).getHot();//获取列表中热度最低的视频的热度值minHot，用于后续在Redis中移除热度低于此值的视频。
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (HotVideo hotVideo : hotVideos) {
                final Double hot = hotVideo.getHot();
                try {
                    //后续给前端的时候再设置上
                    hotVideo.setHot(null);
                    // 不这样写铁报错！序列化问题        管道传输，效率快
                    connection.zAdd(key, hot, jackson2JsonRedisSerializer.serialize(om.writeValueAsString(hotVideo)));
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
            return null;
        });
        redisTemplate.opsForZSet().removeRangeByScore(RedisConstant.HOT_RANK, minHot, 0);
        //从 Redis 中的有序集合中移除热度值介于 0 到 minHot 之间的视频。
        //移除有序集合中热度值低于minHot的视频。这是为了保持有序集合中只存储热度最高的视频信息。


    }

    // 热门视频,没有热度排行榜实时且重要 刷新热门视频
    @Scheduled(cron = "0 0 */3 * * ?")
//    @Scheduled(cron = "0/2 * * * * ?")
    public void hotVideo() {
        // 分片查询3天内的视频
        int limit = 1000;
        long id = 1;
        List<Video> videos = videoService.selectNDaysAgeVideo(id, 999, limit);
        //热度限制
        final Double hotLimit = settingService.list(new LambdaQueryWrapper<Setting>()).get(0).getHotLimit();
        Calendar calendar = Calendar.getInstance();
        int today = calendar.get(Calendar.DATE);

        while (!ObjectUtils.isEmpty(videos)) {
            final ArrayList<Long> hotVideos = new ArrayList<>();
            for (Video video : videos) {
                Long shareCount = video.getShareCount();
                Double historyCount = video.getHistoryCount() * 0.8;
                Long startCount = video.getStartCount();
                Double favoritesCount = video.getFavoritesCount() * 1.5;
                final Date date = new Date();
                //当前-发布
                long t = date.getTime() - video.getGmtCreated().getTime();
                //毫秒数 t 转换为天数
                final double hot = hot(shareCount + historyCount + startCount + favoritesCount, TimeUnit.MILLISECONDS.toDays(t));
//                log.info("{}视频的热度：{}：",video,hot);
                // 大于X热度说明是热门视频
                if (hot > hotLimit) {
                    hotVideos.add(video.getId());
                }

            }
            id = videos.get(videos.size() - 1).getId();
            //3天
            videos = videoService.selectNDaysAgeVideo(id, 7, limit);
            // RedisConstant.HOT_VIDEO + 今日日期 作为key  达到元素过期效果
            if (!ObjectUtils.isEmpty(hotVideos)) {
                String key = RedisConstant.HOT_VIDEO + today;
                redisTemplate.opsForSet().add(key, hotVideos.toArray(new Object[hotVideos.size()]));
                redisTemplate.expire(key, 7, TimeUnit.DAYS);//7
            }

        }


    }

    static double a = 0.011;

    public static double hot(double weight, double t) {
        return weight * Math.exp(-a * t);
    }

    /**
     * 随着时间的增加，-a * t 中的指数部分变小，导致 Math.exp(-a * t) 的值逐渐减小。 e^-1
     * 当时间增长到一定程度，Math.exp(-a * t) 的值减少到初始值的一半时，即 Math.exp(-a * t) = 0.5，这时就达到了半衰期。
     * 因此，整个公式 weight * Math.exp(-a * t) 描述了随着时间 t 的增加，初始值 weight 减少到一半所对应的数值。
     */


    public double weightRandom() {
        int i = (int) ((Math.random() * 9 + 1) * 100000);
        return i / 1000000.0;
    }

}
